iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0
Software Development

掌握Java神器,駕馭SpringBoot猛獸系列 第 26

第26日 從實務看IoC容器與Bean

  • 分享至 

  • xImage
  •  

昨日利用Spring Securtiy添加自定義的帳使用者驗證流程,為了方便說明沒走正常的登入流程,今天來實作登入註冊API,順便透過實作說明Spring IOC與依賴性注入

IoC容器 與 Bean元件

當Spring Boot應用程式啟動時會尋找特定註釋的類別,這種類別在整個應用程式被視為Bean元件,即交由IoC保管的實體物件,這種註釋可以用兩種方式分類

  • 一個類別就是一個 Bean元件

    1. Controler
    2. Component
    3. Service
    4. Repository
    5. Mapper(MyBatis)
  • 配置很多個Bean元件

    1. Configure

這個自動將Bean元件放入IoC容器的操作,被定義在主應用程式中添加的註釋@SpringBootApplication中,組合的註釋有@ComponentScan(自動掃描)以及@EnableAutoConfiguration(自動註冊)兩個註釋,是實現Spring Boot實現自動配置的核心配置

IoC容器能對滿足外部注入條件的地方,帶入相應資料,這部分包含了類別的欄位、方法、建構子,這個設計雖然可以很方便實現依賴反轉,但要注意有個限制,接收依賴物件的實體與給別人使用的實體,都必須由IoC容器管理,換句話說,實現依賴性注入的元件必需是Bean元件

簡單來說IoC容器解決了應用程式會使用到的元件,會依照類別內的屬性或方法參數宣告方式,決定是否由外部注入實體物件,這種讓第三者操作的設計模式,就是第12日介紹到的代理模式,細部的執行則是透過工廠模式來創建實體物件

建立用戶註冊Service

為了方便辨識另外做一個用戶端資料的DTO物件,這裡叫他RegisterMember

// RegisterMember.java
@NoArgsConstructor
@Getter
public class RegisterMember {

    private String account;

    private String password;
    
    private String name;
}

建立一個MemberService來處理登入註冊相關邏輯

@Service
public class MemberService {

    /**
     * @param data
     */
    @Autowired
    private MemberDao memberMapper;

    /**
    * 使用Spring Security的工具進行加密
    */
    @Autowired
    private PasswordEncoder pwdEncoder;

    /**
     * 會員註冊
     * 
     * @param data
     * 
     * @return void
     * @throws Exception
     */
    public void register(RegisterMember data) throws Exception {
        Member oldMember = memberMapper.getByAccount(data.getAccount());

        // Old Memebr 存在表示帳號重複使用
        if (oldMember != null) {
            throw new Exception("帳號已存在");
        }

        // 確定帳號沒問題,將密碼雜湊並建立新帳號
        String password = pwdEncoder.encode(data.getPassword());
        Member newMember = Member.builder()
                .name(data.getName())
                .account(data.getAccount())
                .password(password).build();

        memberMapper.create(newMember);
    }
}

建立 MemberController

@Controller
@RequestMapping(path = "/api/member")
public class MemberController {

    @Autowired
    private MemberService memberServ;

    @PostMapping("/register")
    public ResponseEntity<Object> resister(
            @RequestBody RegisterMember data) {

        try {
            memberServ.register(data);
        } catch (Exception ex) {
            return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(Map.of(
                    "status", false,
                    "message", "註冊失敗"));
        }

        return ResponseEntity.status(HttpStatus.OK).body(Map.of(
                "status", true,
                "message", "註冊成功"));
    }
}

啟用應用程式發生錯誤

https://ithelp.ithome.com.tw/upload/images/20231009/20161920BPSopzKxdJ.png

找不到PasswordEncoder的實體物件,由於是由第三方提供元件,必須在@Configure類別定義方法,實現可以添加的Bean元件,因此打開SecurityConf,定義方法回傳PasswordEncoder介面,這邊使用BCryptPasswordEncoder物件進行加解密

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

補充,先前將 /api/member 為前綴的請求,都限制實現登入驗證,這裡順便說明,可以在會員登入和註冊的請求路徑,額外添加忽略驗證配置,要注意必須放在 /api/member/** 規則之前

// 新的驗證規格
authorize
        .requestMatchers("/api/member/register").permitAll()
        .requestMatchers("/api/member/**").authenticated()
        .anyRequest().permitAll();

重新運行 mvn spring-boor:run 啟用成功,打開Postman用先前測試的 test帳號註冊,顯示註冊失敗

https://ithelp.ithome.com.tw/upload/images/20231009/20161920sQVAQTuYpg.png

在換新的帳號 newTest 註冊,資料表插入新帳號 newTest,並將密碼先雜湊在插入,到這裡註冊流程完成

https://ithelp.ithome.com.tw/upload/images/20231009/201619203sHPNOVFLm.png

實現登入

登入的部分可以藉由Spring Security提供的 AuthenticationManager 進行密碼驗證,因此登入流程時建立 login路由,透過 AuthenticationManager 對要驗證的實體進行驗證

// MemberController 添加login方法
@PostMapping("/login")
public ResponseEntity<Object> login(
        @RequestBody LoginMember data) {
    try {
        // UsernamePasswordAuthenticationToken 建構子帶兩個參數,表示當前實體需要驗證
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                data.getAccount(), data.getPassword());
        authenticationManager.authenticate(authentication);

        String logingToken = jwtUtil.generateToken(data.getAccount());
        return ResponseEntity.status(HttpStatus.OK).body(Map.of(
                "status", true,
                "token", logingToken));
    } catch (Exception ex) {
        System.out.println(ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(Map.of(
                "status", false,
                "message", "登入失敗"));
    }
}

在 SecurityConf 配置 AuthenticationProvider 和 AuthenticationManager 元件


    /**
     * 這裡先記重點
     * 
     * 1. 目前只有一個 DBUserDetailsService 會從 member 資料表抓登入資料
     * 2. 套用 org.springframework.security.core.userdetails.UserDetailsService
     *
    */
    @Autowired
    public UserDetailsService dbUserService;

    @Bean
    public AuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(dbUserService);
        return provider;
    }

    @Bean
    public AuthenticationManager authManager(AuthenticationConfiguration authConf) throws Exception {
        return authConf.getAuthenticationManager();
    }

自己動作手登入驗證時,要從唯一的使用者帳號當作查詢條件,到DB抓取相應使用者資料,並將查詢結果進行解密,在將值跟客戶端輸入的密碼進行比對,兩個值相符才算登入成功

在控制器中使用的 authenticate 方法就是對這個步驟的封裝,差異在於要先將使用者登入資料,先轉換成身分驗證物件 UsernamePasswordAuthenticationToken(注意此處只有兩個參數),作為參數帶入 authenticate的方法中

取得帳號資料的邏輯,封裝在UserDetailsService介面要求的 loadUserByUsername 方法中,因此內部會注入 UserDetailsService 實體物件(目前只有一個從member資料表拿資料),透過帳號取得用戶資料

打開Postman對newTest帳號進行測試,注意密碼是用註冊API添加的,記得輸入自己設定的密碼

輸入錯誤密碼

https://ithelp.ithome.com.tw/upload/images/20231009/20161920s1hgzunfXL.png

輸入正確密碼成功回傳登入 token

https://ithelp.ithome.com.tw/upload/images/20231009/20161920fQrhmTAJ3C.png


上一篇
第25日 用Spring Security做登入驗證
下一篇
第27日 搭配Spring IoC做Comamnd指令
系列文
掌握Java神器,駕馭SpringBoot猛獸30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言